/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import com.crunch.network.util.BitStreamWriter;

import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Queue;

/**
 * TODO: describe protocol
 *
 * [header][unused_bits][messages ...]
 * unused_bits is a 3-bit unsigned integer representing number of bits at the end of the packet to ignore, ranging from 0-7
 * this space is reserved, and after all messages have been written it is filled in
 *
 * mention packet write started and ended
 */
public class NetworkWriter {
	/**
	 * Constructs a {@code NetworkWriter} instance with a reference to the message registry and network settings.
	 *
	 * @param   messageRegistry a reference to the message registry.
	 */
	NetworkWriter(MessageRegistry messageRegistry, NetworkSettings settings) {
		error = false;

		this.messageRegistry = messageRegistry;
		this.settings = settings;

		// compute ghost ID bits - we're the writer so we use max local ghosts
		ghostIdBits = 1;
		int ghostRange = 2;
		while (ghostRange < settings.maxLocalGhosts) {
			++ghostIdBits;
			ghostRange *= 2;
		}

		packet = null;

		packetStarted = false;
		readyToSend = false;
		nextSequenceNumber = 0;

		final int EXPIRATION_TIME_PADDING_MS = 2000;
		messageDependencyGraph = new MessageDependencyGraph(settings.connectionTimeoutMs + EXPIRATION_TIME_PADDING_MS) {
			@Override
			public void executeMessage(int messageId) {
				NetworkWriter.this.executeMessage(messageId);
			}
		};

		guaranteedMessages = new HashMap<Integer, GuaranteedMessage>();
		newGuaranteedMessages = new ArrayDeque<Integer>();
		excessGuaranteedMessages = new ArrayDeque<Integer>();
		newStateTables = new ArrayDeque<StateTable>();
		packets = new HashMap<Integer, Packet>();
	}

	/**
	 * Returns whether the writer has previously encountered an error.
	 *
	 * @return  whether the writer has previously encountered an error.
	 */
	public boolean hasError() {
		return error;
	}

	/**
	 * Calls {@link NetworkWriter#writeMessage(Message, boolean, MessageDependency[], MessageDependency)} with {@code
	 * dependenciesIn} and {@code dependencyOut} set to {@code null}.
	 */
	public boolean writeMessage(Message message, boolean guaranteed) {
		return writeMessage(message, guaranteed, null, null);
	}

	/**
	 * Calls {@link NetworkWriter#writeMessage(Message, boolean, MessageDependency[], MessageDependency)} with {@code
	 * dependencyOut} set to {@code null}.
	 */
	public boolean writeMessage(Message message, boolean guaranteed,
	                            MessageDependency[] dependenciesIn) {
		return writeMessage(message, guaranteed, dependenciesIn, null);
	}

	/**
	 * Calls {@link NetworkWriter#writeMessage(Message, boolean, MessageDependency[], MessageDependency)} with {@code
	 * dependenciesIn} set to {@code null}.
	 */
	public boolean writeMessage(Message message, boolean guaranteed,
	                            MessageDependency dependencyOut) {
		return writeMessage(message, guaranteed, null, dependencyOut);
	}

	/**
	 * Writes the given message to the packet if there is space available and returns whether the message was written.
	 * If no space is available and {@code guaranteed} is {@code false}, the message is discarded. If {@code guaranteed}
	 * is {@code true}, the message is queued and sent in the next packet (with available space).
	 *
	 * A reference to {@code message} is retained, so {@code message} should not be altered after being passed into
	 * this method. When {@code message} is no longer used, a call to {@link MessageRegistry.MessageFactory#destroy
	 * (Message)} is automatically made and it is safe to alter {@code message} after this point. If {@code
	 * guaranteed} is {@code false}, {@link MessageRegistry.MessageFactory#destroy(Message)} will be called
	 * immediately before the method terminates.
	 *
	 * @param   message the message to write to the packet.
	 * @param   guaranteed whether the message should be sent in guaranteed mode.
	 * @param   dependenciesIn a list of message dependencies which should be executed before this one.
	 * @param   dependencyOut the location to store dependency information for this message.
	 * @return  whether the message was written to the packet.
	 * @throws  IllegalArgumentException if {@code guaranteed} is {@code false} and {@code dependencyOut} is not {@code
	 *          null}, the call to {@link Message#getId()} returns an ID which has not been registered with the {@link
	 *          MessageRegistry}, or a dependency in {@code dependenciesIn} belongs to a different {@code NetworkWriter}
	 *          instance.
	 * @throws  IllegalStateException if packet writing hasn't been started.
	 */
	public boolean writeMessage(Message message, boolean guaranteed,
	                            MessageDependency[] dependenciesIn, MessageDependency dependencyOut) {
		if (error) {
			return false;
		}

		if (!packetStarted) {
			throw new IllegalStateException("Packet writing has not been started");
		}

		if (!guaranteed && dependencyOut != null) {
			throw new IllegalArgumentException("Non-guaranteed messages cannot store dependencies");
		}

		if (dependenciesIn != null) {
			for (MessageDependency dep : dependenciesIn) {
				if (!dep.isEmpty() && dep.getWriter() != this) {
					throw new IllegalArgumentException("Invalid message dependency");
				}
			}
		}

		StateTableMessage stMessage = null;
		if (messageRegistry.getRegisteredType(message.getId()) == MessageRegistry.RegisteredType.STATE_TABLE) {
			stMessage = (StateTableMessage) message;
			// do some checks to make sure parameters are correct
			if (stMessage.getMessageType() == StateTableMessage.MessageType.CREATE) {
				assert(guaranteed);
			} else if (stMessage.getMessageType() == StateTableMessage.MessageType.UPDATE) {
				assert(!guaranteed);
			} else if (stMessage.getMessageType() == StateTableMessage.MessageType.DESTROY) {
				assert(guaranteed);
			}
		}

		int networkId = messageRegistry.getNetworkMessageId(message.getId());
		if (networkId < 0) {
			throw new IllegalArgumentException("Unregistered message type");
		}

		BitStreamWriter bitStreamWriter = new BitStreamWriter(
				ByteBuffer.wrap(packet.getPacketData()),
				packet.getPacketContentOffset() * Byte.SIZE + writeOffset,
				packet.getPacketDataSize() * Byte.SIZE);

		// count up the dependencies which aren't empty
		int nonEmptyDependencyCount = 0;

		// when we're writing to the packet, make sure we don't go over the limit
		boolean writeSuccess;
		try {
			// write the message network ID
			bitStreamWriter.writeUnsignedInt(networkId, messageRegistry.getNetworkIdBits());

			// write whether this message is guaranteed
			bitStreamWriter.writeBoolean(guaranteed);

			if (guaranteed) {
				if (guaranteedMessages.containsKey(nextSequenceNumber)) {
					throw new NetworkException("Duplicate guaranteed message specified");
				}

				// write sequence number
				// make sure it is unsigned by removing possible sign extension
				bitStreamWriter.writeUnsignedInt((long) nextSequenceNumber & 0x00000000ffffffffL, settings.messageSequenceNumberBits);
			}

			// write dependencies
			int depCount = (dependenciesIn == null) ? 0 : dependenciesIn.length;
			for (int i = 0; i < depCount; ++i) {
				if (!dependenciesIn[i].isEmpty()) {
					++nonEmptyDependencyCount;
					if (i != 0) {
						// write a single bit indicating that there is another dependency following
						bitStreamWriter.writeBoolean(true);
					}
					// write the dependency
					bitStreamWriter.writeUnsignedInt((long) dependenciesIn[i].getSequenceNumber() & 0x00000000ffffffffL, settings.messageSequenceNumberBits);
				}
			}
			// write a 0 bit to indicate end of dependency list
			bitStreamWriter.writeBoolean(false);

			// finally, write the message content
			message.write(bitStreamWriter);

			writeSuccess = true;
		} catch (NetworkException e) {
			error = true;
			settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
			return false;
		} catch (BufferOverflowException e) {
			// the message doesn't fit in the packet
			// don't update buffer's state
			writeSuccess = false;
		}

		if (writeSuccess) {
			// message was successfully written - update the writeOffset
			writeOffset = bitStreamWriter.position() - (packet.getPacketContentOffset() * Byte.SIZE);
		}

		if (guaranteed) {
			// create a guaranteed message even if we didn't successfully write (in this case queue the message)
			GuaranteedMessage gMsg = new GuaranteedMessage();
			gMsg.sequenceNumber = nextSequenceNumber;
			gMsg.message = message;
			// build list of non-empty dependencies - directly store sequence numbers
			if (nonEmptyDependencyCount > 0) {
				gMsg.dependenciesIn = new int[nonEmptyDependencyCount];
				int nextDep = 0;
				for (MessageDependency dep : dependenciesIn) {
					if (!dep.isEmpty()) {
						gMsg.dependenciesIn[nextDep] = dep.getSequenceNumber();
						++nextDep;
					}
				}
			}
			gMsg.dependencyOut = dependencyOut;
			if (dependencyOut != null) {
				dependencyOut.set(this, nextSequenceNumber);
			}
			// initially not in dependency graph - added when sent with packet
			gMsg.inDependencyGraph = false;
			guaranteedMessages.put(nextSequenceNumber, gMsg);
			if (writeSuccess) {
				// if we successfully wrote, add to the queue of messages for this packet
				newGuaranteedMessages.add(nextSequenceNumber);
			} else {
				// if we didn't successfully write, queue the message for the next packet
				excessGuaranteedMessages.add(nextSequenceNumber);
			}

			// increment sequence number, allowing it to wrap using masking
			nextSequenceNumber = (nextSequenceNumber + 1) & ~(0xffffffff << settings.messageSequenceNumberBits);
		}

		// track state table if necessary
		if (stMessage != null) {
			if (stMessage.getMessageType() == StateTableMessage.MessageType.CREATE) {
				// we wrote a creation message - set variables to pending (initially they are all dirty)
				// first, release references to those elements from old packets
				for (int i = 0; i < stMessage.getStateTable().getElementCount(); ++i) {
					StateElement element = stMessage.getStateTable().getStateElement(i);
					if (element.getPacketId() != StateElement.QUEUED_PACKET) {
						reducePacketStateElements(element.getPacketId(), 1);
					}
					element.queueForPacket();
				}
				if (writeSuccess) {
					// only add to packet if we successfully wrote
					newStateTables.add(stMessage.getStateTable());
				}
			} else if (stMessage.getMessageType() == StateTableMessage.MessageType.UPDATE) {
				// we wrote an update message, or attempted to - only set elements to pending if it was written
				if (writeSuccess) {
					// first, release references to those elements from old packets
					for (int i = 0; i < stMessage.getStateTable().getElementCount(); ++i) {
						StateElement element = stMessage.getStateTable().getStateElement(i);
						if (StateElement.shouldBeSent(false, element.getNetworkTransmissionMode(), element.isDirty())) {
							if (element.getPacketId() != StateElement.QUEUED_PACKET) {
								reducePacketStateElements(element.getPacketId(), 1);
							}
							element.queueForPacket();
						}
					}
					newStateTables.add(stMessage.getStateTable());
				}
			}
		}

		if (!guaranteed) {
			// free the message - we don't retain a reference to it
			messageRegistry.destroyMessage(message);
		}

		return writeSuccess;
	}

	/**
	 * Writes a ghost creation message to the packet using the given ghost ID and state table if there is space
	 * available and returns whether the message was written. Ghost creation messages are guaranteed.
	 *
	 * @param   stateTable the state table associated with the ghost.
	 * @param   ghostId the ID of the ghost.
	 * @param   dependenciesIn a list of message dependencies which should be executed before this one.
	 * @param   dependencyOut the location to store dependency information for this message.
	 * @return  whether the message was written to the packet.
	 * @throws  IllegalArgumentException if an invalid ghost ID is specified, the call to {@link StateTable#getId()}
	 *          returns an ID which has not been registered with the {@link MessageRegistry}, or a dependency in {@code
	 *          dependenciesIn} belongs to a different {@code NetworkWriter} instance.
	 * @throws  IllegalStateException if packet writing hasn't been started.
	 */
	boolean writeCreateGhost(StateTable stateTable, int ghostId,
	                         MessageDependency[] dependenciesIn, MessageDependency dependencyOut) {
		// verify ghost ID
		if (ghostId >= settings.maxLocalGhosts) {
			throw new IllegalArgumentException("Invalid local ghost ID " + ghostId);
		}
		// create a message for the state table
		StateTableMessage message = (StateTableMessage) messageRegistry.createMessage(stateTable.getId());
		message.init(stateTable, ghostId, ghostIdBits, StateTableMessage.MessageType.CREATE);

		return writeMessage(message, true, dependenciesIn, dependencyOut);
	}

	// todo document
	boolean writeUpdateGhost(StateTable stateTable, int ghostId,
	                         MessageDependency[] dependenciesIn) {
		// verify ghost ID
		if (ghostId >= settings.maxLocalGhosts) {
			throw new IllegalArgumentException("Invalid local ghost ID " + ghostId);
		}
		// create a message for the state table
		StateTableMessage message = (StateTableMessage) messageRegistry.createMessage(stateTable.getId());
		message.init(stateTable, ghostId, ghostIdBits, StateTableMessage.MessageType.UPDATE);

		return writeMessage(message, false, dependenciesIn);
	}

	// todo document
	boolean writeDestroyGhost(StateTable stateTable, int ghostId,
	                          MessageDependency[] dependenciesIn, MessageDependency dependencyOut) {
		// verify ghost ID
		if (ghostId >= settings.maxLocalGhosts) {
			throw new IllegalArgumentException("Invalid local ghost ID " + ghostId);
		}
		// create a message for the state table
		StateTableMessage message = (StateTableMessage) messageRegistry.createMessage(stateTable.getId());
		message.init(stateTable, ghostId, ghostIdBits, StateTableMessage.MessageType.DESTROY);

		return writeMessage(message, true, dependenciesIn, dependencyOut);
	}

	// rewrites a message to the packet - called internally when a packet containing a guaranteed message is dropped
	// this method doesn't modify any message queues - should be done externally
	private boolean rewriteGuaranteedMessage(Message message, int sequenceNumber, int[] dependenciesIn) {
		int networkId = messageRegistry.getNetworkMessageId(message.getId());
		if (networkId < 0) {
			throw new IllegalArgumentException("Unregistered message type");
		}

		StateTableMessage stMessage = null;
		if (messageRegistry.getRegisteredType(message.getId()) == MessageRegistry.RegisteredType.STATE_TABLE) {
			stMessage = (StateTableMessage) message;
			// do some checks to make sure parameters are correct
			assert(stMessage.getMessageType() == StateTableMessage.MessageType.CREATE ||
					stMessage.getMessageType() == StateTableMessage.MessageType.DESTROY);
		}

		BitStreamWriter bitStreamWriter = new BitStreamWriter(
				ByteBuffer.wrap(packet.getPacketData()),
				packet.getPacketContentOffset() * Byte.SIZE + writeOffset,
				packet.getPacketDataSize() * Byte.SIZE);

		// when we're writing to the packet, make sure we don't go over the limit
		boolean writeSuccess;
		try {
			// write the message network ID
			bitStreamWriter.writeUnsignedInt(networkId, messageRegistry.getNetworkIdBits());

			// write whether this message is guaranteed (it always is in this method)
			bitStreamWriter.writeBoolean(true);

			// write sequence number
			// make sure it is unsigned by removing possible sign extension
			bitStreamWriter.writeUnsignedInt((long) sequenceNumber & 0x00000000ffffffffL,
					settings.messageSequenceNumberBits);

			// write dependencies
			int depCount = (dependenciesIn == null) ? 0 : dependenciesIn.length;
			for (int i = 0; i < depCount; ++i) {
				if (i != 0) {
					// write a single bit indicating that there is another dependency following
					bitStreamWriter.writeBoolean(true);
				}
				// write the dependency
				bitStreamWriter.writeUnsignedInt((long) dependenciesIn[i] & 0x00000000ffffffffL,
						settings.messageSequenceNumberBits);
			}
			// write a 0 bit to indicate end of dependency list
			bitStreamWriter.writeBoolean(false);

			// finally, write the message content
			message.write(bitStreamWriter);

			writeSuccess = true;
		} catch (BufferOverflowException e) {
			// the message doesn't fit in the packet
			// don't update buffer's state
			writeSuccess = false;
		}

		if (writeSuccess) {
			// message was successfully written - update the writeOffset
			writeOffset = bitStreamWriter.position() - (packet.getPacketContentOffset() * Byte.SIZE);
		}

		// guaranteed message was already created before, so no need to re-create it

		// track state table if necessary
		if (stMessage != null) {
			if (stMessage.getMessageType() == StateTableMessage.MessageType.CREATE) {
				// we wrote a creation message - set variables to pending (initially they are all dirty)
				// first, release references to those elements from old packets
				for (int i = 0; i < stMessage.getStateTable().getElementCount(); ++i) {
					StateElement element = stMessage.getStateTable().getStateElement(i);
					if (element.getPacketId() != StateElement.QUEUED_PACKET) {
						reducePacketStateElements(element.getPacketId(), 1);
					}
					element.queueForPacket();
				}
				if (writeSuccess) {
					// only add to packet if we successfully wrote
					newStateTables.add(stMessage.getStateTable());
				}
			}
		}

		return writeSuccess;
	}

	/**
	 * Returns the number of bytes currently used for packet content.
	 *
	 * @return  the number of bytes currently used for packet content.
	 */
	public int getContentSize() {
		// round up to the next byte
		return (writeOffset + Byte.SIZE - 1) / Byte.SIZE;
	}

	/**
	 * Prepares the packet for writing. Guaranteed messages which could not fit in the previous packet are
	 * automatically rewritten at this point (if there is sufficient space).
	 *
	 * @throws  IllegalStateException if packet writing has already been started or has already ended.
	 */
	public void startPacket() {
		if (error) {
			return;
		}

		if (packet == null) {
			throw new IllegalStateException("No packet assigned");
		}

		if (packetStarted) {
			throw new IllegalStateException("Packet writing has already been started");
		}
		if (readyToSend) {
			throw new IllegalStateException("Packet writing has already ended");
		}

		packetStarted = true;

		// reset write offset to the beginning of the content (plus 3 bits for the "unused bits" value)
		writeOffset = 3;

		// write as many queued excess guaranteed messages as possible
		boolean full = false;
		while (!full && !excessGuaranteedMessages.isEmpty()) {
			// try rewriting the guaranteed message - don't remove from queue yet though, since it might fail
			GuaranteedMessage gMsg = guaranteedMessages.get(excessGuaranteedMessages.peek());
			if (rewriteGuaranteedMessage(gMsg.message, gMsg.sequenceNumber, gMsg.dependenciesIn)) {
				// success - remove from the excess messages queue and add to the packet queue
				newGuaranteedMessages.add(excessGuaranteedMessages.remove());
			} else {
				// can't write any more messages - don't remove from excess queue, save it for next packet
				full = true;
			}
		}
	}

	/**
	 * Called when all messages have been written to the packet. The "bits remaining" field is filled in here.
	 *
	 * @throws  IllegalStateException if packet writing hasn't been started or if packet writing has already ended.
	 */
	public void endPacket() {
		if (error) {
			return;
		}

		if (!packetStarted) {
			throw new IllegalStateException("Packet writing has not been started");
		}
		if (readyToSend) {
			throw new IllegalStateException("Packet writing has already ended");
		}

		int bitsRemaining = (getContentSize() * Byte.SIZE) - writeOffset;
		assert(bitsRemaining >= 0 && bitsRemaining < 8); // make sure we can fit into 3 bits

		BitStreamWriter bsw = new BitStreamWriter(
				ByteBuffer.wrap(packet.getPacketData()),
				packet.getPacketContentOffset() * Byte.SIZE,
				packet.getPacketContentOffset() * Byte.SIZE + 3);
		// write 3 bits (0-7) indicating how many unused bits there are in the last byte of the packet
		bsw.writeUnsignedInt(bitsRemaining, 3);

		packetStarted = false;
		readyToSend = true;
	}

	/**
	 * Called to clear the packet after it has been sent.
	 */
	void clearPacket() {
		if (error) {
			return;
		}

		packetStarted = false;
		readyToSend = false;
	}

	/**
	 * Returns whether the packet has been started.
	 *
	 * @return  whether the packet has been started.
	 */
	boolean isPacketStarted() {
		return packetStarted;
	}

	/**
	 * Returns whether the writer is ready to send.
	 *
	 * @return  whether the writer is ready to send.
	 */
	boolean isReadyToSend() {
		return readyToSend;
	}

	/**
	 * Sets the packet to write to.
	 *
	 * @param   packet the packet to write to.
	 */
	void setPacket(NetworkPacket packet) {
		this.packet = packet;
	}

	/**
	 * Called periodically to allow messages to expire in the message dependency graph.
	 *
	 * @param   currentTimeMs the current time in milliseconds.
	 */
	void update(long currentTimeMs) {
		if (error) {
			return;
		}

		try {
			messageDependencyGraph.expireMessages(currentTimeMs);
		} catch (NetworkException e) {
			error = true;
			settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
		}
	}

	/**
	 * Called directly after a packet has been successfully sent.
	 *
	 * @param   packetId the ID of the packet which was just sent.
	 * @param   currentTimeMs the current time in milliseconds.
	 * @throws  IllegalStateException if packet writing was not ended.
	 */
	void onPacketSent(int packetId, long currentTimeMs) {
		if (error) {
			return;
		}

		if (readyToSend) {
			throw new IllegalStateException("Packet was not ready to send");
		}

		// check if the packet needs to be tracked
		if (!newGuaranteedMessages.isEmpty() || !newStateTables.isEmpty()) {
			Packet packet = new Packet();

			if (!newGuaranteedMessages.isEmpty()) {
				// build the message list for the packet
				packet.guaranteedMessages = new int[newGuaranteedMessages.size()];
				for (int i = 0; i < newGuaranteedMessages.size(); ++i) {
					packet.guaranteedMessages[i] = newGuaranteedMessages.remove();
					// add to dependency graph if not yet in
					GuaranteedMessage gMsg = guaranteedMessages.get(packet.guaranteedMessages[i]);
					if (!gMsg.inDependencyGraph) {
						try {
							messageDependencyGraph.addMessage(packet.guaranteedMessages[i], gMsg.dependenciesIn, currentTimeMs);
						} catch (NetworkException e) {
							error = true;
							settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
							return;
						}
						gMsg.inDependencyGraph = true;
					}
				}

				newGuaranteedMessages.clear();
			}

			if (!newStateTables.isEmpty()) {
				// build the state table list for the packet
				packet.stateTables = new StateTable[newStateTables.size()];
				packet.stateTableElements = 0;
				for (int i = 0; i < newStateTables.size(); ++i) {
					packet.stateTables[i] = newStateTables.remove();
					// count the pending elements in the state table
					// assign the packet ID
					for (int e = 0; e < packet.stateTables[i].getElementCount(); ++e) {
						StateElement element = packet.stateTables[i].getStateElement(e);
						if (element.isPending() && element.getPacketId() == StateElement.QUEUED_PACKET) {
							++packet.stateTableElements;
							element.setPacketId(packetId);
						}
					}
				}

				newStateTables.clear();
			}

			// store the packet
			packets.put(packetId, packet);
		}

		readyToSend = false;
	}

	/**
	 * Called when a previously sent packet has been acknowledged - i.e. it has been confirmed that the packet was
	 * received.
	 *
	 * @param   packetId the ID of the acknowledged packet.
	 */
	void onPacketAcknowledged(int packetId) {
		if (error) {
			return;
		}

		// if we weren't tracking the packet, do nothing
		Packet packet = packets.remove(packetId);
		if (packet == null) {
			return;
		}

		// acknowledge each component of the packet

		if (packet.guaranteedMessages != null) {
			for (int i = 0; i < packet.guaranteedMessages.length; ++i) {
				// mark as ready to execute and evaluate it in the mirrored dependency graph
				try {
					messageDependencyGraph.setMessageReadyToExecute(packet.guaranteedMessages[i]);
					// when executeMessage() is called, the guaranteed message is freed, so no need to do it here
				} catch (NetworkException e) {
					error = true;
					settings.callbacks.log(NetworkCallbacks.LogLevel.ERROR, e.getMessage());
					return;
				}
			}
		}

		if (packet.stateTables != null) {
			for (int i = 0; i < packet.stateTables.length; ++i) {
				StateTable st = packet.stateTables[i];
				for (int e = 0; e < st.getElementCount(); ++e) {
					StateElement element = st.getStateElement(e);
					// if the element was pending in this packet, clear its pending flag
					if (element.isPending() && element.getPacketId() == packetId) {
						element.clearPending();
					}
				}
			}
		}
	}

	/**
	 * Called when a previously sent packet has been dropped.
	 *
	 * @param   packetId the ID of the dropped packet.
	 */
	void onPacketDropped(int packetId) {
		if (error) {
			return;
		}

		// if we weren't tracking the packet, do nothing
		Packet packet = packets.remove(packetId);
		if (packet == null) {
			return;
		}

		// mark elements that were sent in the packet as dirty so that they will be resent
		if (packet.stateTables != null) {
			for (int i = 0; i < packet.stateTables.length; ++i) {
				StateTable st = packet.stateTables[i];
				for (int e = 0; e < st.getElementCount(); ++e) {
					StateElement element = st.getStateElement(e);
					// if the element was pending in this packet, clear its pending flag
					if (element.isPending() && element.getPacketId() == packetId) {
						element.clearPending();
						element.setDirty();
					}
				}
			}
		}

		// go through the list of guaranteed messages in the packet and resend them
		if (packet.guaranteedMessages != null) {
			for (int i = 0; i < packet.guaranteedMessages.length; ++i) {
				GuaranteedMessage gMsg = guaranteedMessages.get(packet.guaranteedMessages[i]);
				// rewrite each guaranteed message
				rewriteGuaranteedMessage(gMsg.message, gMsg.sequenceNumber, gMsg.dependenciesIn);
			}
		}
	}

	/**
	 * Called by a {@link MessageDependency} instance to release the {@code NetworkWriter}'s internal reference to
	 * that {@link MessageDependency} instance.
	 *
	 * @param   dependency the dependency to "forget".
	 */
	void forgetMessageDependency(MessageDependency dependency) {
		// should only be called by non-empty dependencies pointing to this writer
		assert !dependency.isEmpty() && dependency.getWriter() == this;

		GuaranteedMessage gMsg = guaranteedMessages.get(dependency.getSequenceNumber());
		if (gMsg != null) {
			// could gMsg ever be null? maybe not
			// "forget" the dependency by setting it to null
			gMsg.dependencyOut = null;
			// now, when the guaranteed message is executed, it won't try to clear a dependency that doesn't exist
		}
	}

	// called when a message can be executed in the dependency graph
	// this occurs when it has been detected that this message and all messages it depends on have been received
	private void executeMessage(int sequenceNumber) {
		// this message has been executed on the remote side
		// therefore, future messages no longer need a dependency on it, so we clear the dependency
		GuaranteedMessage gMsg = guaranteedMessages.get(sequenceNumber);
		if (gMsg.dependencyOut != null) {
			gMsg.dependencyOut.clear();
			// calling the clear() method will call forgetMessageDependency, which accesses guaranteedMessages and set dependency to null
			// this is why we didn't yet call remove() (only get()) yet
			// also why we don't have to explicitly set gMsg.dependency = null
		}

		// handle ghost-related messages
		if (messageRegistry.getRegisteredType(gMsg.message.getId()) == MessageRegistry.RegisteredType.STATE_TABLE) {
			StateTableMessage stm = (StateTableMessage) gMsg.message;
			if (stm.getMessageType() == StateTableMessage.MessageType.DESTROY) {
				// we can free up the state table, since the DESTROY message has been executed on the remote side
				// this means we will never need to resend the state table data
				StateTable st = stm.getStateTable();
				messageRegistry.destroyStateTable(st);
			}
		}

		// no longer need the message instance
		messageRegistry.destroyMessage(gMsg.message);
		guaranteedMessages.remove(sequenceNumber);
	}

	// when a state table element is sent in a new packet, this method is called for its previous packet
	// this is so that state table elements are only tracked in one packet
	// if a packet becomes "empty" due to this call, it is removed
	private void reducePacketStateElements(int packetId, int elementCount) {
		Packet packet = packets.get(packetId);
		packet.stateTableElements -= elementCount;
		assert(packet.stateTableElements >= 0);

		// if the packet has no state table elements, clear the state table list
		if (packet.stateTableElements == 0) {
			packet.stateTables = null;
		}

		// remove the packet if it is entirely empty
		if (packet.guaranteedMessages == null && packet.stateTables == null) {
			packets.remove(packetId);
		}
	}

	private boolean error;

	private MessageRegistry messageRegistry;
	private NetworkPacket packet;
	private NetworkSettings settings;
	private int ghostIdBits;

	// whether packet writing has been started
	private boolean packetStarted;
	// whether the packet is ready to send
	private boolean readyToSend;
	// write offset from the beginning of the packet's content
	private int writeOffset;
	// sequence number of next message
	private int nextSequenceNumber;

	// graph which keeps track of which guaranteed messages have arrived so that dependencies can be removed
	private MessageDependencyGraph messageDependencyGraph;

	// a guaranteed message which has not yet been verified as having been executed on the receiving side
	private static class GuaranteedMessage {
		// sequence number of the message
		public int sequenceNumber;

		// the message
		public Message message;

		// the message's dependencies
		public int[] dependenciesIn;

		// reference to the (optional) message dependency for this message
		// once a message has been confirmed executed, this dependency is cleared and "dependency" is set to null
		public MessageDependency dependencyOut;

		// whether this message has been added to the dependency graph yet
		public boolean inDependencyGraph;
	}

	// maps sequence numbers to guaranteed messages
	private HashMap<Integer, GuaranteedMessage> guaranteedMessages;

	// queue of new guaranteed messages currently written to the packet but not yet sent
	private Queue<Integer> newGuaranteedMessages;

	// queue of guaranteed messages which didn't fit into the current packet - they will be automatically written to the next one
	private Queue<Integer> excessGuaranteedMessages;

	// queue of new state tables currently written to the packet but not yet sent
	private Queue<StateTable> newStateTables;

	// a packet which has been previously sent and contains data that must be tracked, such as guaranteed messages
	private static class Packet {
		// list of guaranteed message sequence numbers contained in this packet
		public int[] guaranteedMessages;
		// list of state tables contained in this packet
		public StateTable[] stateTables;
		// number of total elements from state tables tracked in this packet
		public int stateTableElements;
	}

	// maps packet ID to packets
	private HashMap<Integer, Packet> packets;
}
